<<<<<<< HEAD ======= >>>>>>> origin/main CS01: Biomarkers of Recent Use

Load packages

library(tidyverse)
library(ggplot2)

Introduction

In this case study, we seek to determine the best biomarkers of recent THC use within a 3-hour period by evaluating compounds across three matrices: blood, oral fluid, and breath. Additionally, we aim to determine the optimal THC concentration cutoffs for each matrix within this timeframe and to examine how treatment groups may influence THC levels. This analysis is important given the legal implications of THC detection in impaired driving cases. Nineteen states have implemented per se or zero-tolerance laws for THC, with per se limits varying between 1-5 ng/mL THC in blood 1. These limits suggest significant regulatory interest in identifying reliable biological markers of recent cannabis use, particularly in roadside testing contexts.

The study’s dataset includes three matrices–blood, oral fluid, and breath–with a total of 573 participants across them. Blood and oral fluid matrices allow detection of multiple THC-related compounds (8 in blood, 7 in oral fluid), while breath analysis focuses solely on one compound. Blood and oral fluid matrices have previously demonstrated relatively stable THC levels over time, providing a basis for exploring how they reflect THC concentrations within a 3-hour window 2. Breath testing, however, remains a newer field of investigation, and its reliability for THC detection within a short timeframe is still under review. Identifying the optimal biological matrix for THC detection in recent use is important for law enforcement to establish reliable, science-based standards for roadside impairment testing.

The analysis also seeks to address the legal challenges posed by varying state regulations. For example, states like Colorado utilize a “reasonable inference” approach, where blood concentrations above 5 ng/ml THC suggest impairment 3. These differing legal standards show the importance of identifying clear, evidence-based cutoff values for THC detection across different matrices. In this case study, we aim to determine which matrix is most effective for detecting recent THC use within a 3-hour window and to identify the optimal cutoff values for each matrix. This analysis will help us understand how different matrices perform in detecting THC levels over time and provide insights into their potential effectiveness in practical settings.

Questions

  1. Which matrix–blood, oral fluid, or breath–is most effective for detecting recent THC use within a 3-hour window?
  2. What is the optimal cutoff within this 3-hour timeframe for detecting THC in each matrix?
  3. How does the treatment group (Placebo, 5.9% THC, or 13.4% THC) affect the detection of recent THC use in each matrix?

The Data

Data Import

<<<<<<< HEAD
WB <- read_csv("data/Blood.csv")
BR <- read_csv("data/Breath.csv")
OF <- read_csv("data/OF.csv")
=======
WB <- read_csv("data/Blood.csv")
## Rows: 1525 Columns: 14
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (5): ID, Treatment, Group, FLUID TYPE, Timepoint
## dbl (9): CBN, CBD, THC, 11-OH-THC, THC-COOH, THC-COOH-Gluc, CBG, THC-V, time...
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
BR <- read_csv("data/Breath.csv")
## Rows: 949 Columns: 7
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (5): ID, Treatment, Group, Fluid, Timepoint
## dbl (2): THC (pg/pad), time.from.start
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
OF <- read_csv("data/OF.csv")
## Rows: 953 Columns: 13
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (5): ID, Treatment, Group, Fluid, Timepoint
## dbl (8): CBN, CBD, THC, 11-OH-THC, CBG, THC-V, THCA-A, time.from.start
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
>>>>>>> origin/main

Data Wrangling

WB <- WB |> 
  mutate(Treatment = fct_recode(Treatment, 
                                "5.9% THC (low dose)" = "5.90%",
                                "13.4% THC (high dose)" = "13.40%"),
         Treatment = fct_relevel(Treatment, "Placebo", "5.9% THC (low dose)")) |> 
  janitor::clean_names() |>
  rename(thcoh = x11_oh_thc,
         thccooh = thc_cooh,
         thccooh_gluc = thc_cooh_gluc,
         thcv = thc_v) |>
  mutate(timepoint = case_when(time_from_start < 0 ~ "pre-smoking",
                               time_from_start > 0 & time_from_start <= 60 ~ "0-60 min",
                               time_from_start > 61 & time_from_start <= 120 ~ "0-120 min",
                               time_from_start > 121 & time_from_start <= 180 ~ "0-180 min",
                               time_from_start > 180 ~ "181+ min"))

OF <- OF |> 
  mutate(Treatment = fct_recode(Treatment, 
                                "5.9% THC (low dose)" = "5.90%",
                                "13.4% THC (high dose)" = "13.40%"),
         Treatment = fct_relevel(Treatment, "Placebo", "5.9% THC (low dose)")) |> 
  janitor::clean_names() |>
  rename(thcoh = x11_oh_thc,
         thcv = thc_v,
         fluid_type = fluid) |>
  mutate(timepoint = case_when(time_from_start < 0 ~ "pre-smoking",
                               time_from_start > 0 & time_from_start <= 60 ~ "0-60 min",
                               time_from_start > 61 & time_from_start <= 120 ~ "0-120 min",
                               time_from_start > 121 & time_from_start <= 180 ~ "0-180 min",
                               time_from_start > 180 ~ "181+ min"))

BR <- BR |> 
  mutate(Treatment = fct_recode(Treatment, 
                                "5.9% THC (low dose)" = "5.90%",
                                "13.4% THC (high dose)" = "13.40%"),
         Treatment = fct_relevel(Treatment, "Placebo", "5.9% THC (low dose)")) |> 
  janitor::clean_names() |>
  rename(thc = thc_pg_pad,
         fluid_type = fluid) |>
  mutate(timepoint = case_when(time_from_start < 0 ~ "pre-smoking",
                               time_from_start > 0 & time_from_start <= 60 ~ "0-60 min",
                               time_from_start > 61 & time_from_start <= 120 ~ "0-120 min",
                               time_from_start > 121 & time_from_start <= 180 ~ "0-180 min",
                               time_from_start > 180 ~ "181+ min"))

combined <- bind_rows(list(WB, OF, BR))
write_csv(combined, "combined_data.csv")
combined_long <- combined |> 
  select(1:5,time_from_start,everything()) |>
  pivot_longer(7:15)

invisible(ggplot(combined_long, aes(x = value)) + 
  geom_histogram() +
  facet_wrap(~name))

invisible(combined_long |>
  mutate(group_compound = paste0(fluid_type, ": ", name)) |>
  ggplot(aes(x = value)) + 
  geom_histogram() + 
  facet_wrap(~group_compound, scales = "free"))

invisible(combined_long |> 
  filter(name == "thc") |>
  ggplot(aes(x = group, y = value)) + 
  geom_boxplot() +
  facet_wrap(~fluid_type, scales = "free"))

invisible(combined_long |> 
  filter(name == "thc") |>
  ggplot(aes(x = treatment, y = value)) + 
  geom_boxplot() +
  facet_wrap(~fluid_type, scales = "free"))

invisible(combined_long |> 
  filter(name == "thc", (timepoint == "0-30 min" | timepoint == "0-40 min")) |>
  ggplot(aes(x = treatment, y = value)) + 
  geom_boxplot() +
  facet_wrap(~fluid_type, scales = "free"))

plot_scatter_time <- function(matrix) {
  combined_long |> 
      filter(!is.na(time_from_start), fluid_type==matrix) |>
      ggplot(aes(x=time_from_start, y=value, color=group)) + 
        geom_point() +
        facet_wrap(~name, scales="free") +
        scale_color_manual(values=c("#19831C", "#A27FC9")) +
        theme_classic() +
        labs(title=paste("Compound Levels Across Time for", matrix),
             x="Time From Start (min)",
             y="Concentration (pg/mL)") +
        theme(legend.position="bottom",
              legend.title=element_blank(),
              strip.background=element_blank(),
              plot.title.position="plot") 
}

plot_scatter_time(matrix = "WB")
<<<<<<< HEAD <<<<<<< HEAD

plot_scatter_time(matrix = "OF")

plot_scatter_time(matrix = "BR")

=======
## Warning: Removed 1526 rows containing missing values or values outside the scale range
## (`geom_point()`).

plot_scatter_time(matrix = "OF")
## Warning: Removed 1938 rows containing missing values or values outside the scale range
## (`geom_point()`).

plot_scatter_time(matrix = "BR")
## Warning: Removed 7592 rows containing missing values or values outside the scale range
## (`geom_point()`).

>>>>>>> origin/main =======

plot_scatter_time(matrix = "OF")

plot_scatter_time(matrix = "BR")

>>>>>>> ff15d15ccadd72846c98eb72dbee8f1dd4c289ca
ggplot(combined, aes(x = time_from_start, y = thc)) +
  geom_point(size = 1, color = "lightgreen") +
  facet_wrap(~ fluid_type, scales = "free_y") +

  labs(
    title = "THC Levels across Time in each Fluid Type",
    x = "Time from Start (minutes)",
    y = "Concentration (pg/mL)",
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5),
    legend.position = "top"
  )
<<<<<<< HEAD <<<<<<< HEAD

=======
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_point()`).

>>>>>>> origin/main
ggplot(combined, aes(x = timepoint, y = thc, color = treatment, group = treatment)) +
  geom_line() +
  geom_point(position = position_jitter(width = 0.2), size = 2) +  # Add jitter for better visibility
  facet_wrap(~ fluid_type, scales = "free_y") +  # Create separate plots for each fluid type
  labs(
    title = "THC Levels Across Fluid Types Over Time",
    x = "Time Interval",
    y = "THC Level",
    color = "Dosage"
  ) +
  theme_minimal(base_size = 14) +  # Increase base font size for readability
  theme(
    plot.title = element_text(hjust = 0.5),
    axis.text.x = element_text(angle = 45, hjust = 1),  # Rotate x-axis labels for readability
    legend.position = "top"
  )
<<<<<<< HEAD

=======

>>>>>>> origin/main
ggplot(combined %>% filter(fluid_type == "WB" & time_from_start >= 0 & time_from_start <= 180), aes(x = time_from_start, y = thc, color = treatment, group = treatment)) +
  geom_line() +
  geom_point(position = position_jitter(width = 0.2), size = 2) +  # Add jitter for better visibility
  labs(
    title = "THC Levels Across Water Blood Over Time",
    x = "Time Interval",
    y = "THC Level",
    color = "Dosage"
  ) +
  theme_minimal(base_size = 14) +  # Increase base font size for better readability
  theme(
    plot.title = element_text(hjust = 0.5),
    axis.text.x = element_text(angle = 45, hjust = 1),  # Rotate x-axis labels
    legend.position = "bottom"
  )
<<<<<<< HEAD

=======

>>>>>>> origin/main
ggplot(combined %>% filter(fluid_type == "WB" & time_from_start >= 0 & time_from_start <= 60), aes(x = time_from_start, y = thc, color = treatment, group = treatment)) +
  geom_line() +
  geom_point(position = position_jitter(width = 0.2), size = 2) +  # Add jitter for better visibility
  labs(
    title = "THC Levels Across Water Blood Over Time",
    x = "Time Interval",
    y = "THC Level",
    color = "Dosage"
  ) +
  theme_minimal(base_size = 14) +  # Increase base font size for better readability
  theme(
    plot.title = element_text(hjust = 0.5),
    axis.text.x = element_text(angle = 45, hjust = 1),  # Rotate x-axis labels
    legend.position = "bottom"
  )
<<<<<<< HEAD

=======

>>>>>>> origin/main
ggplot(combined %>% filter(fluid_type == "WB" & time_from_start >= 0 & time_from_start <= 60), aes(x = time_from_start, y = thc, color = treatment, group = treatment)) +
  geom_line() +
  geom_point(position = position_jitter(width = 0.2), size = 2) +
  facet_wrap(~treatment) +
  labs(
    title = "THC Levels Across Water Blood Over Time",
    x = "Time Interval",
    y = "THC Level",
    color = "Dosage"
  ) +
  theme_minimal(base_size = 14) +  # Increase base font size for better readability
  theme(
    plot.title = element_text(hjust = 0.5),
    axis.text.x = element_text(angle = 45, hjust = 1),  # Rotate x-axis labels
    legend.position = "bottom"
  )
<<<<<<< HEAD

=======

```

>>>>>>> origin/main =======

>>>>>>> ff15d15ccadd72846c98eb72dbee8f1dd4c289ca

Analysis

Results & Discussion

Conclusion


  1. Pearson, J., et al. (2020). Legal implications of THC per se limits. Journal of Accident Analysis & Prevention. Retrieved from Taylor & Francis Online.↩︎

  2. Huestis, M. A. (2007). Human cannabinoid pharmacokinetics. Chemistry & Biodiversity, 4(8), 1770-1804. [https://pmc.ncbi.nlm.nih.gov/articles/PMC2689518/).↩︎

  3. Compton, R. (2017). Marijuana-impaired driving: A report to Congress. National Highway Traffic Safety Administration. Retrieved from NGTSA.↩︎